package com.hsu.hsurpc.registry;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ConcurrentHashSet;
import cn.hutool.cron.CronUtil;
import cn.hutool.cron.task.Task;
import cn.hutool.json.JSONUtil;
import com.hsu.hsurpc.config.RegistryConfig;
import com.hsu.hsurpc.model.ServiceMetaInfo;
import io.etcd.jetcd.*;
import io.etcd.jetcd.options.GetOption;
import io.etcd.jetcd.options.PutOption;
import io.etcd.jetcd.watch.WatchEvent;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Etcd 注册中心实现类
 * @Author Hsu琛君珩
 * @Date 2024-09-20 16:55
 * @Description
 * @Version: v1.0.0
 */
public class EtcdRegistry implements Registry {

    /**
     * Etcd 客户端实例
     */
    private Client client;

    /**
     * KV 客户端，用于与 Etcd 进行键值操作
     */
    private KV kvClient;

    /**
     * Etcd 根节点路径，防止不同项目的数据冲突
     */
    private static final String ETCD_ROOT_PATH = "/rpc/";

    /**
     * 本机注册的节点 key 集合（用于维护续期）
     */
    private final Set<String> localRegisterNodeKeySet = new HashSet<>();

    /**
     * 注册中心服务缓存（支持多个服务键）
     */
    private final RegistryServiceMultiCache registryServiceMultiCache = new RegistryServiceMultiCache();

    /**
     * 正在监听的 key 集合
     */
    private final Set<String> watchingKeySet = new ConcurrentHashSet<>();

    /**
     * 初始化 Etcd 客户端，连接到注册中心
     *
     * @param registryConfig 注册中心的配置信息，包含连接地址、超时时间等
     */
    @Override
    public void init(RegistryConfig registryConfig) {
        client = Client.builder()
                .endpoints(registryConfig.getAddress())                           // 使用配置的地址
                .connectTimeout(Duration.ofMillis(registryConfig.getTimeout()))   // 设置超时时间
                .build();
        kvClient = client.getKVClient(); // 获取 KV 操作客户端
        // 启动心跳机制，定期检查并续签服务节点的租约
        heartBeat();
    }

    /**
     * 注册服务到 Etcd 注册中心
     * 服务注册通过键值对的方式存储，并为服务设置租约（过期时间），
     * 同时将注册的服务节点信息添加到本地缓存以便后续管理。
     *
     * @param serviceMetaInfo 服务的元数据信息（包括服务名称、版本、地址、端口等）
     * @throws Exception 如果注册失败，抛出异常
     */
    @Override
    public void register(ServiceMetaInfo serviceMetaInfo) throws Exception {
        // 设置注册时间
        serviceMetaInfo.setRegisterTime(LocalDateTime.now());
        // 创建 Lease 和 KV 客户端
        Lease leaseClient = client.getLeaseClient();

        // 创建 30 秒租约，用于设置服务节点的过期时间
        long leaseId = leaseClient.grant(30).get().getID();

        // 设置要存储的键值对，键为服务节点的唯一键名
        String registerKey = ETCD_ROOT_PATH + serviceMetaInfo.getServiceNodeKey(); // /rpc/com.hsu.example.common.service.UserService:1.0/localhost:8080
        ByteSequence key = ByteSequence.from(registerKey, StandardCharsets.UTF_8);
        ByteSequence value = ByteSequence.from(JSONUtil.toJsonStr(serviceMetaInfo), StandardCharsets.UTF_8);

        // 使用租约设置键值对并存储到 Etcd，并设置过期时间
        PutOption putOption = PutOption.builder().withLeaseId(leaseId).build();
        kvClient.put(key, value, putOption).get();

        // 添加节点信息到本地缓存
        localRegisterNodeKeySet.add(registerKey);
    }

    /**
     * 注销服务节点
     * 从 Etcd 注册中心中删除已注册的服务节点信息，并从本地缓存中移除该节点的键名。
     *
     * @param serviceMetaInfo 要注销的服务的元数据信息
     */
    @Override
    public void unRegister(ServiceMetaInfo serviceMetaInfo) {
        // 构建服务节点的唯一键名，格式为 "/rpc/{serviceName}:{version}/{host}:{port}"
        String registerKey = ETCD_ROOT_PATH + serviceMetaInfo.getServiceNodeKey();
        // 从 Etcd 中删除与该服务节点对应的键值对
        kvClient.delete(ByteSequence.from(registerKey, StandardCharsets.UTF_8));
        // 从本地缓存移除
        localRegisterNodeKeySet.remove(registerKey);
    }

    /**
     * 服务发现，根据服务键名前缀，查找已注册的所有服务节点信息
     *
     * @param serviceKey 服务的唯一键名（通常由服务名称和版本号组成）
     * @return 返回该服务下的所有节点列表
     */
    @Override
    public List<ServiceMetaInfo> serviceDiscovery(String serviceKey) {
        // 优先从缓存获取服务
        List<ServiceMetaInfo> cachedServiceMetaInfoList = registryServiceMultiCache.readCache(serviceKey);
        if (cachedServiceMetaInfoList != null) {
            System.out.println("发现服务 [" + serviceKey + "] 使用缓存，服务节点信息：" + cachedServiceMetaInfoList);
            return cachedServiceMetaInfoList;
        }
        // 从注册中心查询服务节点
        String searchPrefix = ETCD_ROOT_PATH + serviceKey;
        try {
            GetOption getOption = GetOption.builder().isPrefix(true).build();
            List<KeyValue> keyValues = kvClient.get(
                            ByteSequence.from(searchPrefix, StandardCharsets.UTF_8),
                            getOption)
                    .get()
                    .getKvs();
            List<ServiceMetaInfo> serviceMetaInfoList = keyValues.stream()
                    .map(keyValue -> {
                        String key = keyValue.getKey().toString(StandardCharsets.UTF_8);
                        watch(key);  // 监听 key 的变化，这里监听的是完整的 key：/rpc/com.hsu.example.common.service.UserService:1.0:localhost:8080
                        String value = keyValue.getValue().toString(StandardCharsets.UTF_8);
                        return JSONUtil.toBean(value, ServiceMetaInfo.class);
                    })
                    .collect(Collectors.toList());
            serviceMetaInfoList.forEach(service -> {
                System.out.println("Service registered at: " + service.getRegisterTime());
            });
            System.out.println("发现服务 [" + serviceKey + "] 不使用缓存，服务节点信息：" + serviceMetaInfoList);
            registryServiceMultiCache.writeCache(serviceKey, serviceMetaInfoList);
            return serviceMetaInfoList;
        } catch (Exception e) {
            throw new RuntimeException("获取服务列表失败", e);
        }
    }

    /**
     * 销毁注册中心，释放 Etcd 客户端资源
     * 在项目关闭或服务停止时调用此方法，断开与 Etcd 注册中心的连接，
     * 下线所有已注册的服务节点，并释放相关的客户端资源。
     */
    @Override
    public void destroy() {
        System.out.println("当前节点下线");
        // 下线节点：遍历本节点所有的 key 并删除
        for (String key : localRegisterNodeKeySet) {
            try {
                kvClient.delete(ByteSequence.from(key, StandardCharsets.UTF_8)).get();
            } catch (Exception e) {
                throw new RuntimeException(key + "节点下线失败", e);
            }
        }

        // 释放 KV 客户端和 Etcd 客户端的资源
        if (kvClient != null) {
            kvClient.close();
        }
        if (client != null) {
            client.close();
        }
    }

    /**
     * 定期发送心跳信号，保持服务节点的存活状态
     * 通过定时任务，每隔 10 秒遍历本地注册的所有服务节点，检查它们在 Etcd 中的存活状态，
     * 如果节点未过期，则重新注册该节点（续签租约），以延长其在 Etcd 中的存活时间。
     * 如果节点已过期，则不进行续签处理。
     */
    @Override
    public void heartBeat() {
        // 使用 CronUtil 设置一个每 10 秒执行一次的定时任务
        CronUtil.schedule("*/10 * * * * *", new Task() {
            @Override
            public void execute() {
                // 遍历本地缓存中所有已注册的服务节点键
                for (String key : localRegisterNodeKeySet) {
                    try {
                        // 从 Etcd 中获取对应的服务节点信息
                        List<KeyValue> keyValues = kvClient.get(ByteSequence.from(key, StandardCharsets.UTF_8))
                                .get()
                                .getKvs();
                        // 检查该节点是否已过期
                        if (CollUtil.isEmpty(keyValues)) {
                            // 如果节点已过期，跳过续签
                            continue;
                        }
                        // 如果节点未过期，进行重新注册以续签租约
                        KeyValue keyValue = keyValues.get(0);
                        String value = keyValue.getValue().toString(StandardCharsets.UTF_8);
                        ServiceMetaInfo serviceMetaInfo = JSONUtil.toBean(value, ServiceMetaInfo.class);
                        register(serviceMetaInfo);
                    } catch (Exception e) {
                        throw new RuntimeException(key + "续签失败", e);
                    }
                }
            }
        });
        // 支持秒级精度的定时任务
        CronUtil.setMatchSecond(true);
        CronUtil.start();
    }

    /**
     * 监听指定服务节点的变化
     * 通过 Etcd 的 Watch 机制，监控指定服务节点的状态变化（如删除事件），
     * 当服务节点发生变化时，执行相应的处理逻辑。
     *
     * @param serviceNodeKey 需要监听的服务节点的键名
     */
    @Override
    public void watch(String serviceNodeKey) {
        Watch watchClient = client.getWatchClient();
        // 之前未被监听，开启监听
        boolean newWatch = watchingKeySet.add(serviceNodeKey);
        if (newWatch) {
            watchClient.watch(ByteSequence.from(serviceNodeKey, StandardCharsets.UTF_8), response -> {
                for (WatchEvent event : response.getEvents()) {
                    switch (event.getEventType()) {
                        // key 被删除时触发
                        case DELETE:
                            String serviceKey = extractServiceKey(serviceNodeKey);
                            // 清理注册服务缓存
                            System.out.println("服务节点 [" + serviceKey + "] 被删除，清理缓存");
                            registryServiceMultiCache.clearCache(serviceKey);
                            break;
                        case PUT:
                        default:
                            break;
                    }
                }
            });
        }
    }

    // 提取出serviceKey的方法
    private String extractServiceKey(String serviceNodeKey) {
        // 假设 serviceNodeKey 的格式是 /rpc/serviceName:version:host:port
        // 我们需要提取出 serviceName:version
        int startIndex = ETCD_ROOT_PATH.length(); // 去掉 /rpc/ 前缀
        int endIndex = serviceNodeKey.lastIndexOf(':'); // 找到最后一个冒号
        if (endIndex > startIndex) {
            endIndex = serviceNodeKey.lastIndexOf(':', endIndex - 1); // 找到倒数第二个冒号
            return serviceNodeKey.substring(startIndex, endIndex); // 提取出 serviceKey
        } else {
            return serviceNodeKey.substring(startIndex); // 如果没有找到冒号，返回去掉前缀的部分
        }
    }
}